package org.u2ml.stack.web.rest;

import java.util.HashSet;
import java.util.Set;

import javax.servlet.ServletContext;
import javax.ws.rs.Path;
import javax.ws.rs.core.Application;
import javax.ws.rs.ext.Provider;

import org.apache.commons.lang.StringUtils;
import org.restlet.Context;
import org.restlet.ext.jaxrs.InstantiateException;
import org.restlet.ext.jaxrs.JaxRsApplication;
import org.restlet.ext.jaxrs.ObjectFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.u2ml.Main;

import com.noelios.restlet.ext.servlet.ServletContextAdapter;

/**
 * Rest application, providing JaxRs restlet implementation and spring
 * integration, together with auto discovery of jax-rs resources and providers.
 * <p>
 * Resources must be annotated with {@link Path} to be discovered
 * </p>
 * <p>
 * They must be annotated with {@link Component} to be obtained with Spring bean
 * factory. To mimic the behaviour of regular restlet, the scope prototype is
 * recommended.
 * </p>
 * <p>
 * Providers must be annotated with {@link Provider} to be discovered
 * </p>
 */
public class XRestApplication extends JaxRsApplication {
	/**
	 * Parameter name to tunnel POST into other method (PUT, DELETE, etc ...)
	 */
	public static final String TUNNEL_METHOD_PARAM = "x:method";
		
	/**
	 * Auto discovery jax-rs application config.
	 * <p>
	 * It is based on spring classpath scanning.
	 * </p>
	 * <p>
	 * Resources must be annotated with {@link Path} to be discovered
	 * </p>
	 * <p>
	 * Providers must be annotated with {@link Provider} to be discovered. If
	 * annotated with {@link Component}, spring will be used to provide the
	 * instance in the JaxRs app config.
	 * </p>
	 */
	private final class JaxRsAppConfig extends Application {
		private BeanFactory beanFactory;
		
		public JaxRsAppConfig(BeanFactory beanFactory) {
			this.beanFactory = beanFactory;
		}
		public Set<Class<?>> getClasses() {
			Set<Class<?>> rrcs = new HashSet<Class<?>>();
			ClassPathScanningCandidateComponentProvider entitiesScanner = 
				new ClassPathScanningCandidateComponentProvider(false);

			entitiesScanner.addIncludeFilter(new AnnotationTypeFilter(Path.class));
			logger.debug("scanning classpath for JAX-RS resources...");
			Set<BeanDefinition> candidates = entitiesScanner.findCandidateComponents(
					Main.class.getPackage().getName());
			logger.info("found {} JAX-RS resources", candidates.size());

			for (BeanDefinition candidate : candidates) {
				try {
					// register the class as a JaxRs resource
					Class<?> clazz = Class.forName(candidate.getBeanClassName());
					rrcs.add(clazz);
				} catch (ClassNotFoundException e) {
					logger.warn("resource class found cannot be loaded: {}", 
							candidate.getBeanClassName());
				}
			}
			return rrcs;
		}
		
		@Override
		public Set<Object> getSingletons() {
			Set<Object> singletons = new HashSet<Object>();
			ClassPathScanningCandidateComponentProvider entitiesScanner = 
				new ClassPathScanningCandidateComponentProvider(false);

			entitiesScanner.addIncludeFilter(new AnnotationTypeFilter(Provider.class));
			logger.debug("scanning classpath for JAX-RS providers...");
			Set<BeanDefinition> candidates = entitiesScanner.findCandidateComponents(Main.class.getPackage().getName());
			logger.info("found {} JAX-RS providers", candidates.size());

			for (BeanDefinition candidate : candidates) {
				try {
					// register an instance of the class as a JaxRs singleton
					Class<?> clazz = Class.forName(candidate.getBeanClassName());
					Component c = clazz.getAnnotation(Component.class);
					if (c != null) {
						String name = getBeanName(clazz, c);
						singletons.add(beanFactory.getBean(name, clazz));
					} else {
						// default to default constructor instantiation
						singletons.add(clazz.newInstance());
					}
				} catch (ClassNotFoundException e) {
					logger.warn("provider class found cannot be loaded: {}", 
							candidate.getBeanClassName());
				} catch (InstantiationException e) {
					logger.warn("provider class found cannot be instantiated: " + 
							candidate.getBeanClassName(), e);
				} catch (IllegalAccessException e) {
					logger.warn("provider class found cannot be instantiated: " + 
							candidate.getBeanClassName(), e);
				}
			}
			return singletons;
		}
	}

	private final Logger logger = LoggerFactory.getLogger(XRestApplication.class);
	
	public XRestApplication(Context context) {
        super(context);

        final BeanFactory beanFactory = getBeanFactory(context);
		
		setObjectFactory(new ObjectFactory() {
			@SuppressWarnings("unchecked")
			public <T> T getInstance(Class<T> jaxRsClass) throws InstantiateException {
				Component c = jaxRsClass.getAnnotation(Component.class);
				if (c != null) {
					String name = getBeanName(jaxRsClass, c);
					return (T) beanFactory.getBean(name, jaxRsClass);
				} else {
					// default to regular JaxRs construction
					return null;
				}
			}
		});
		
		if (beanFactory.containsBean("guardFactory")) {
			GuardFactory guardFactory = (GuardFactory) beanFactory.getBean("guardFactory", GuardFactory.class);
			setGuard(guardFactory.createGuard(context));
		}

        add(new JaxRsAppConfig(beanFactory));
        getTunnelService().setMethodParameter(TUNNEL_METHOD_PARAM);
    }

	private BeanFactory getBeanFactory(Context context) {
		ServletContext ctx = ((ServletContextAdapter)context).getServletContext();
		return WebApplicationContextUtils.getWebApplicationContext(ctx);
	}

	private <T> String getBeanName(Class<T> clazz, Component component) {
		String name = (component.value() == null || component.value().length() == 0) 
						? StringUtils.uncapitalize(clazz.getSimpleName()) 
						: component.value();
		return name;
	}
}